package ${package.Security};

import lombok.extern.slf4j.Slf4j;
import ${package.Stereotype}.security.filter.JwtAuthorizationFilter;
import ${package.Stereotype}.security.filter.VerifyCodeFilter;
import ${package.Stereotype}.security.handler.RestfulAccessDeniedHandler;
import ${package.Stereotype}.security.impl.CacheServiceImpl;
import ${package.Stereotype}.security.bean.AbstractUser;
import ${package.Stereotype}.security.impl.AbstractUserGetter;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.CachingUserDetailsService;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.util.Assert;

import javax.annotation.Resource;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * <p>
 *     Spring Security 权限控制类
 * </p>
 *
 * @EnableWebSecurity :开启SpringSecurity的默认行为。
 * @EnableGlobalMethodSecurity: 在Spring Security中提供了一些访问控制的注解。这些注解都是默认是都不可用的，需要通过@EnableGlobalMethodSecurity进行开启后使用
 *     如果设置的条件允许，程序正常执行。如果不允许会报500
 *     securedEnabled = true  prePostEnabled = true  开启基于方法级别的校验方式,可以把下面的注解加在  Service接口或方法上上也可以写到Controller或Controller的方法上
 *  控制这个URL是否允许被访问。
 *     1. @Secured : 是专门用于判断是否具有角色的。能写在方法或类上。角色参数需要使用 ROLE_开头。
 *     2. @PreAuthorize和@PostAuthorize:
 *          @PreAuthorize表示访问方法或类在执行之前先判断权限，大多情况下都是使用这个注解，注解的参数和access()方法参数取值相同，都是权限表达式。
 *          @PostAuthorize表示方法或类执行结束后判断权限，此注解很少被使用到。
 */
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig {
    /**
     * 验证码过滤器: 在登录校验之前使用验证码校验信息
     */
    @Resource
    VerifyCodeFilter verifyCodeFilter;

    @Resource
    AuthenticationConfiguration authenticationConfiguration;

    @Resource
    RestfulAccessDeniedHandler restfulAccessDeniedHandler;

    /**
     * 创建一个用户信息获取工具，可以用数据库或者其它地方通过用户名称获取用户的信息
     * 这个应该是 Spring Security与外界查找用户信息交互的入口，它的下一步会通过 {@link UserDetails} 组装一个 {@link User} 对象进行其它处理
     * 用户信息需要继承 {@link AbstractUser}
     * @return {@linkplain AbstractUserGetter 用户信息获取工具}
     */
    @Bean
    @ConditionalOnMissingBean(AbstractUserGetter.class)
    <#assign genericity = 'WebUser' />
    public AbstractUserGetter<${genericity}> defaultUserGetter(){
        /**
         * 初始化几个用户对象
         */
        <#assign genericity = 'String' />
        ConcurrentHashMap<${genericity}, WebUser> map = new ConcurrentHashMap();
        // 测试密码 123456
        WebUser user = new WebUser("Malphite","$2a$10$VEIWFCtghSo0KWhIOSYffepLsHON/mjl.htc0kDmfPYxcCkANqGwW","admin", new ArrayList<>());
        map.put(user.getSecurityUsername(), user);
        return new AbstractUserGetter(){

            @Override
            public WebUser getUser(String username) {
                /**
                 * 从上面的用户中通过用户名称查找用户对象
                 */
                return map.get(username);
            }
        };
    }

    /**
     * 创建一个 {@link UserDetailsService}实例
     * @param getter 用户信息获取工具,需要提前在spring容器中创建
     * @return {@link UserDetailsService}实例
     */
    @Bean
    @ConditionalOnMissingBean(UserDetailsService.class)
    public UserDetailsService defaultUserDetails(AbstractUserGetter getter){

        return new UserDetailsService(){

            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                /**
                 * 通过{@linkplain AbstractUserGetter 用户信息获取工具}获取用户信息
                 */
                AbstractUser user = getter.getUser(username);
                if (user == null) throw new UsernameNotFoundException("用户不存在");
                /**
                 * 填充角色信息并返回 {@link User} 对象
                 */
                <#assign genericity = 'GrantedAuthority' />
                List<${genericity}> grantedAuthorities = new ArrayList<>();
                if (user != null)
                user.getSecurityRole().stream().forEach(roleId -> grantedAuthorities.add(new SimpleGrantedAuthority(roleId)));
                return new User(user.getSecurityUsername(), user.getSecurityPassword(), grantedAuthorities);
            }
        };
    }

    /**
     * <p>
     *     配置SpringSecurity过滤器链,通常多个{@link HttpSecurity} 之间通过注解 @Order 来决定排序,未注解的排序到最后
     * </p>
     *
     *      SecurityFilterChain 里面可能用到下面的的方法:
     *      1. authorizeRequests() 对url进行授权控制
     *      2. antMatcher(HTTP METHOD)  ant规则匹配url  ? 匹配一个字符 * 匹配0个或多个字符 ** 匹配0个或多个目录;传入HTTP METHOD只对指定的方法进行过滤
     *      3. anyRequest()  表示匹配所有的请求
     *      4. regexMatchers(HTTP METHOD) 表示正则表达式进行匹配;传入HTTP METHOD只对指定的方法进行过滤
     *      5. permitAll()     表示所匹配的URL任何人都允许访问
     *      6. authenticated() 表示所匹配的URL都需要被认证才能访问
     *      7. anonymous()     表示可以匿名访问匹配的URL。和permitAll()效果类似，只是设置为anonymous()的url会执行filter 链中
     *      8. denyAll()       表示所匹配的URL都不允许被访问,如果用户未被认证需要认证，如果已经认证，报403
     *      9. rememberMe()    被“remember me”的用户允许访问
     *      10. fullyAuthenticated()     如果用户不是被remember me的，才可以访问。
     *      11. hasAuthority(String)     设置具有 String  权限时才能访问。
     *      12. hasAnyAuthority(String …) 如果用户具备给定权限中某一个，就允许访问
     *      13. hasRole(String)          设置具有给定角色才能访问。 ROLE_ 开头
     *      14. hasAnyRole(String …)     如果用户具备给定角色的任意一个，就允许被访问
     *      15. hasIpAddress(String)     如果请求是指定的IP就运行访问。
     *      16. exceptionHandling().accessDeniedHandler(handler)  自定义403处理方案
     *      17. access("permitAll")  === permitAll()     登录用户权限判断实际上底层实现都是调用access(表达式)
     *      18. access("hasRole('ROLE_admin')")  ===  hasRole("ROLE_admin")    hasRole() 参数是必须不能以ROLE_开头;access中hasRole一定要以ROLE_开头
     *
     *      ** and() 方法的特殊意义。(简单的就是:每当 and 出现,当前的配置项就结束了,可以开启下一个配置了.使用它可以让整个链式的配置更连贯)
     *
     *          在 {@link HttpSecurity } 中的许多配置方法会去调用 {@link HttpSecurity#getOrApply(SecurityConfigurerAdapter)}},
     *      这个方法会返回一个 SecurityConfigurerAdapter对象。它的第二个泛型就是 HttpSecurity。
     *          所有的configurer都有一个共同的父类，那就是 SecurityConfigurer, SecurityConfigurerAdapter在父类的基础上添加了一个and方法，
     *      这个and方法会返回它第二个泛型，也就是当前的 HttpSecurity对象。
     *          所以在进行配置之后调用and方法可以使返回值回到 HttpSecurity对象。从而继续进行下面的配置。
     */
    @Bean
    // @Order(1)
    public SecurityFilterChain configure(HttpSecurity http, AuthenticationSuccessHandler successHandler,
        AuthenticationFailureHandler failureHandler, AuthenticationEntryPoint authenticationEntryPoint,
        LogoutSuccessHandler logoutSuccessHandler , UserDetailsService userDetailsService, CacheServiceImpl cache) throws Exception {
        /**
         * 解决iframe与安全器兼容性问题
         */
        http.headers().frameOptions().disable();
        /**
         * 1.URL权限配置
         */
        http.authorizeRequests()
            /**
             * 2. 匹配/index/vercode请求, -> 5.任何人都允许访问这个地址
             */
            .antMatchers("/index/vercode").permitAll()
            /**
             * 3. 匹配所有的请求, -> 12.需要下面任一一个权限才能访问
             */
            .anyRequest().hasAnyAuthority("user,admin".split(",")).and()
            /**
             * 基于表单登录
             */
            .formLogin()
            /**
             *  登录页访问路径设置 -> 任何人都允许访问这个地址
             */
            .loginPage("/index/login").permitAll()
            /**
             * successHandler  设置登录成功之后的处理器
             * failuerHandler  设置登录失败之后的处理器
             */
            .successHandler(successHandler).failureHandler(failureHandler).and()
            /**
             * 异常处理(权限拒绝、登录失效等) ->  认证自定义异常
             */
            .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
            /**
             * 添加自定义未授权和未登录结果返回 ->  授权自定义异常
             */
            .exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler).and()
            /**
             * addFilterBefore的主要功能是将自定义的过滤器添加到指定过滤器之前
             * 将验证码过滤器添加在UsernamePasswordAuthenticationFilter之前
             */
            .addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class)//验证码
            /**
             * 因为security是一条过滤器链,可以加入任意一个过滤器节点前或者后,我这个就是没有排序的,直接加入filters里
             * 认证是否登录的对象是继承BasicAuthenticationFilter类,(认证方式我使用token认证),通过BasicAuthenticationFilter的子类去做认证,
             */
            .addFilter(new JwtAuthorizationFilter(authenticationConfiguration.getAuthenticationManager(), cachingUserDetailsService(userDetailsService), cache))
            /**
             * session的生成策略，前后端分离的时候配置成STATELESS，非前后端分离的需要配置成ALWAYS
             */
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            /**
             * 设置退出登录路径
             */
            .logout().logoutUrl("/index/logout")
            /**
             *  上面的退出登录操作 是否使session失效，默认为true
             */
            .invalidateHttpSession(true)
            /**
             *  上面的退出登录操作 表示是否清除认证信息，默认为true
             */
            .clearAuthentication(true)
            /**
             * 设置退出登录成功处理器
             * 这个配置与logoutSuccessUrl互斥
             */
            .logoutSuccessHandler(logoutSuccessHandler)
            /**
             * 上面的退出登录操作，清除浏览器缓存
             * 退出时要删除的Cookies的名字
             * permitAll 退出登录的路径 -> 任何人都允许访问这个地址
             */
            .deleteCookies("JSESSIONID").permitAll().and()
            /**
             * 开启跨域
             */
            .cors().and()
            /**
             * 关闭csrf防护
             * 果没有这行代码导致用户无法被认证。
             */
            .csrf().disable();
        return http.build();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /**
     * 此处实现缓存的时候，官方现成的 CachingUserDetailsService ，但是这个类的构造方法不是 public 的，
     * @param delegate
     * @return
     */
    private CachingUserDetailsService cachingUserDetailsService(UserDetailsService delegate) {
    <#assign genericity = 'CachingUserDetailsService' />
        Constructor<${genericity}> ctor = null;
        try {
            ctor = CachingUserDetailsService.class.getDeclaredConstructor(UserDetailsService.class);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        Assert.notNull(ctor, "CachingUserDetailsService constructor is null");
        ctor.setAccessible(true);
        return BeanUtils.instantiateClass(ctor, delegate);
    }
}
